Skip to content

Add DDF zcl:cluster parse fn command - #8631

Open
manup wants to merge 2 commits into
dresden-elektronik:masterfrom
manup:zcl_cluster_fn
Open

Add DDF zcl:cluster parse fn command#8631
manup wants to merge 2 commits into
dresden-elektronik:masterfrom
manup:zcl_cluster_fn

Conversation

@manup

@manup manup commented Jul 6, 2026

Copy link
Copy Markdown
Member

The zcl:cluster DDF parse function allows specifying multiple clusters as filter. Mostly useful for moving switches from button_maps.json to pure DDF implementation.

{"fn": "zcl:cluster", "cl": "0xFC00", "eval": "..."}
{"fn": "zcl:cluster", "cl": ["0xFC00", "0xFC01"], "script": "handler.js"}

Note this function doesn't support specifying cmd, mf and attr, these must be handled in respective Javascript handler. The ep endpoint parameter is 255 (any source endpoint) by default.
Seperate PRs will use this to move switches out of button_maps.json.

Edit: as suggested by @ebaauw there is also a new global Javascript function button_event for declarative handling similar to button_maps.json.

button_event([
  [clusterId, commandId | commandIds[], button | function]
])

Example for Ikea on/off switch:

{
          "name": "state/buttonevent",
          "parse": {
            "fn": "zcl:cluster",
            "cl": ["0x0006", "0x0008"],
            "script": "tradfri_on_off_buttonevent.js"
          },
          "awake": true
}

tradfri_on_off_buttonevent.js

// move 0x01, move w. onoff 0x05, stop w. onoff 0x07
// [clusterId, commandId, button | function]
button_event([
  [6, 1, 1002],
  [6, 0, 2002],
  [8, 5, 1001],
  [8, 7, function(){ return (Item.val === 1001 ? 1003 : 2003); }],
  [8, 1, 2001]
])

This allows specifying multiple clusters as filter. Mostly useful for moving switches from button_maps.json to pure DDF implementation.

{"fn": "zcl:cluster", "cl": "0xFC00", "eval": "..."}
    {"fn": "zcl:cluster", "cl": ["0xFC00", "0xFC01"], "script": "handler.js"}

Note this function doesn't support specifying cmd and attr, this must be handled in respective handler.

Seperate PR's will use this to move switches out of button_maps.json.
@ebaauw

ebaauw commented Jul 7, 2026

Copy link
Copy Markdown
Collaborator

I like this! After fixing #8613, we can ditch button_maps.json.

I would like to take a more declarative approach: define a list of buttonevent mappings per device and a generic function to evaluate the Zigbee command against that list. By using inline functions for exceptions (like the direction), the list entries remain simple: cluster, command, value. Not sure if we have the equivalent of require or import in our JavaScript environment, though.

The device-specific .js would be:

var button_event = require('button_event.js')

button_event([
  [0x0006, 0x01, 1002],
  [0x0006, 0x00, 2002],
  [0x0008, [0x01, 0x05], () => { return ZclFrame.at(0) === 0x00 ? 1001 : 2001 }],
  [0x0008, [0x03, 0x07], () => { return Item.val === 1001 ? 1003 : 2003 }]
])

The generic javascript would be:

function button_event_entry(cluster, cmd, val) {
  if (ClusterId === cluster) {
    if (ZclFrame.cmd === cmd || (Array.isArray(cmd) && cmd.includes(ZclFrame.cmd))) {
      return (typeof val === 'function') ? val() : val
    }
  }
  return null
}

function button_event (list) {
  for (var entry of list) {
    var event = button_event_entry(entry[0], entry[1], entry[2])
    if (event != null) {
      Item.val = event
      return
    }
  }
}

module.export = button_event

Note the use of JavaScript's weak typing to specify command as a simple value or an array, and value as a simple value or inline function.

@manup

manup commented Jul 7, 2026

Copy link
Copy Markdown
Member Author

Good idea I like the declarative approach, it's much cleaner than my if/if else mess. The option to specify a function as third parameter should cover most if not all switches.

I'll update the PR to make your button_event() available at the global scope, no extra import needed. Also gonna look at #8613 asap.

@manup

manup commented Jul 7, 2026

Copy link
Copy Markdown
Member Author

With the new button_event function the handler for Ikea on/off switch looks friendlier.
I changed your function a bit to integrate it in DucktapeJS which lacks a few modern JS features like "for ... in ...", Array.includes and arrow functions, otherwise following version does the same:

const char *PF_button_event = "function button_event(list) {"
                                  "for (var i = 0; i < list.length; i++) { var e = list[i];"
                                  "if (!Array.isArray(e) || e.length !== 3) return;"
                                  "if (ClusterId === e[0] && (ZclFrame.cmd === e[1] || (Array.isArray(e[1]) && e[1].indexOf(ZclFrame.cmd) >= 0))) {"
                                  "Item.val = typeof e[2] === 'function' ? e[2]() : e[2]; return;"
                                  "}} }";

    if (duk_peval_string(ctx, PF_button_event) != 0)
    {
        const char *str = duk_safe_to_string(ctx, -1);
        DBG_Printf(DBG_JS, "failed to define button_event: %s\n", str);
    }
    duk_pop(ctx);

Sucessfully tested with Ikea on/off switch. The state/buttonevent handler becomes:

// move 0x01, move w. onoff 0x05, stop w. onoff 0x07
// [clusterId, commandId, button | function]
button_event([
  [6, 1, 1002],
  [6, 0, 2002],
  [8, 5, 1001],
  [8, 7, function(){ return (Item.val === 1001 ? 1003 : 2003); }],
  [8, 1, 2001]
])

Imho that's less noise compared to former button_maps.json entry:

"ikeaOnOffMap": {
            "vendor": "IKEA",
            "doc": "TRÅDFRI on/off switch",
            "modelids": ["TRADFRI on/off switch"],
            "buttons": [
                {"S_BUTTON_1": "On"},
                {"S_BUTTON_2": "Off"}
            ],
            "map": [
                [1, "0x01", "ONOFF", "ON", "0", "S_BUTTON_1", "S_BUTTON_ACTION_SHORT_RELEASED", "On"],
                [1, "0x01", "LEVEL_CONTROL", "MOVE_WITH_ON_OFF", "0", "S_BUTTON_1", "S_BUTTON_ACTION_HOLD", "Move up (with on/off)"],
                [1, "0x01", "LEVEL_CONTROL", "STOP_WITH_ON_OFF", "0", "S_BUTTON_1", "S_BUTTON_ACTION_LONG_RELEASED", "Stop (with on/off)"],
                [1, "0x01", "ONOFF", "OFF", "0", "S_BUTTON_2", "S_BUTTON_ACTION_SHORT_RELEASED", "Off"],
                [1, "0x01", "LEVEL_CONTROL", "MOVE", "1", "S_BUTTON_2", "S_BUTTON_ACTION_HOLD", "Move down"],
                [1, "0x01", "LEVEL_CONTROL", "STOP_WITH_ON_OFF", "1", "S_BUTTON_2", "S_BUTTON_ACTION_LONG_RELEASED", "Stop"]
            ]
        },

@ebaauw

ebaauw commented Jul 7, 2026

Copy link
Copy Markdown
Collaborator

Cool, looking good.

Note that I used arrays for the Move and Move (with On/Off) as well as for the Stop and Stop (with On/Off) commands. I've seen devices changing their behaviour with newer firmware, as well as with clones of the same OEM device. I find it weird that the IKEA switch sends Move (down) on hold, but Stop (with On/Off) on release. Are we sure that's correct?

@ebaauw

ebaauw commented Jul 7, 2026

Copy link
Copy Markdown
Collaborator

Just realised, we have some switches where the endpoint is used as well. Maybe add a fourth element to the list entries? Logically the endpoint should be the first element, but only a few devices would actually use different endpoints. And probably need an array value for ep as well to handle these. Or can we leave out ep to match all endpoints?

@manup

manup commented Jul 7, 2026

Copy link
Copy Markdown
Member Author

The DateCode of my switch is 20230308 and SwBuildId is 24.4.6, not sure what is different on older/newer versions. Above code does work for all events here. Note the original button map also only had used ZCL commands 0x00, 0x01 and 0x01, 0x05, 0x07 for level control cluster. I also noticed that 0x07 doesn't have any ZCL payload it's just the commandId.

We can add add endpoint to the function (or provide different function since it isn't the common case), there is also the global property SrcEp which can be used in a button function.

@ebaauw

ebaauw commented Jul 7, 2026

Copy link
Copy Markdown
Collaborator

The ep endpoint parameter is 255 (any source endpoint) by default.
there is also the global property SrcEp which can be used in a button function.

That should be good enough, I guess.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants